package com.blz.flare.crawler;

import com.blz.flare.parser.ContentParser;
import com.blz.flare.parser.ParseResult;
import com.blz.flare.solr.SolrUpdaterTool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * A thread which parses a URI.
 * 
 * @author pchapman
 */
//TODO add method to stop the thread on request accessible at package level
public class CrawlerThread extends Thread {

	private static int count = 0;
	private static final Logger logger = LoggerFactory.getLogger(CrawlerThread.class);
	
	private List<Pattern> blackList;
    private List<Pattern> whiteList;
    private Map<String, String> metadataFields;
    private Set<URI> urisProcessed;
    private Queue<URI> urisToProcess;
    private SolrUpdaterTool updaterTool;

    private List<ContentParser> parsers;

    /**
     * Construct a {@link CrawlerThread}.
     *
     * @param tgroup the threadgroup
     * @param config the configuration
     * @param urisToProcess urls to process
     * @param urisProcessed processed urls
     * @throws MalformedURLException on error
     */
	public CrawlerThread(ThreadGroup tgroup, CrawlerConfig config,
            Queue<URI> urisToProcess, Set<URI> urisProcessed)
		    throws MalformedURLException {

		super(tgroup, "Flare Website Crawler Thread " + String.valueOf(++count));
		setDaemon(true);

        // set all fields
		this.blackList = config.getBlackList();
		this.metadataFields = config.getMetaDataFields();
		this.urisToProcess = urisToProcess;
		this.urisProcessed = urisProcessed;
		this.updaterTool = new SolrUpdaterTool(config.getSolrURL());
		this.whiteList = config.getWhiteList();
	}


    /**
     * {@inheritDoc}
     */
	public void run() {
		logger.debug("Starting crawler thread {}", getName());
		String href;
		URI url;
		for (url = urisToProcess.remove(); url != null ; url = urisToProcess.remove()) {
			if (! urisProcessed.contains(url)) {
				urisProcessed.add(url);
				href = url.toString();
                if (canParse(url)) {
                    try {
                        process(url);
                    } catch (Exception ex) {
                        logger.error("Error processing url {}", url, ex);

                    }
                }
			}
		}
		logger.debug("Crawler thread {} stopping", getName());
	}

    /**
     * Process a {@link URI}.
     *
     * @param uri the URI
     * @throws IOException on error
     * @throws PostException on error
     */
	@SuppressWarnings("unchecked")
	private void process(URI uri) throws IOException, PostException {

        // exit early if we can't parse this uri.
        if (!canParse(uri)) {
            return;
        }

        // continue on and parse
		logger.debug("Indexing URI {}", uri);

        // find the appropriate parser(s) and update
        for (ContentParser parser : parsers) {
            if (parser.supports(uri.getScheme())) {
                try {
                    ParseResult result = parser.parse(uri);
                    urisToProcess.addAll(result.getParsedURIs());
                    updaterTool.updateIndexes(uri, result.getTitle(), result.getText(), result.getData());
                } catch (Exception ex) {
                    logger.debug("EXCEPTION parsing: "+uri, ex);
                }
            }
        }
	}

    /**
     * Checks to see if a URI can be parsed.
     *
     * @param uri the uri to check
     * @return true if this uri shouldn't be ignored
     */
    private boolean canParse(URI uri) {
        for (Pattern wp : whiteList) {
            if (wp.matcher(uri.toString()).matches()) {
                for (Pattern bp : blackList) {
                    if (bp.matcher(uri.toString()).matches()) {
                        logger.debug("ignoring blacklisted: "+uri.toString());
                        return false;
                    }
                }
                return true;
            }
            return false;
        }
        return true;
    }

    /**
     * @param parsers the parsers to set
     */
    public void setParsers(List<ContentParser> parsers) {
        this.parsers = parsers;
    }
}
